/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Threading;
using Xceed.Silverlight.Utils;

namespace Xceed.Silverlight.DragDrop
{
  public sealed class DragDropManager
  {
    private DragDropManager()
    {
      ContentPresenter contentPresenter = new ContentPresenter();
      contentPresenter.SetBinding( ContentPresenter.ContentTemplateProperty, new Binding() );

      m_customCursorPopup.Child = contentPresenter;

      string cursorXaml = "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ><Canvas Width='16' Height='16'><Path Data='F1 M 8.000,12.950 C 7.063,12.950 6.195,12.675 5.447,12.221 L 12.222,5.447 C 12.675,6.195 12.950,7.062 12.950,8.000 C 12.950,10.733 10.733,12.950 8.000,12.950 Z M 3.050,8.000 C 3.050,5.266 5.267,3.050 8.000,3.050 C 8.839,3.050 9.617,3.278 10.310,3.646 L 3.646,10.310 C 3.278,9.617 3.050,8.839 3.050,8.000 Z M 8.000,0.000 C 3.582,0.000 0.000,3.582 0.000,8.000 C 0.000,9.687 0.525,11.250 1.417,12.540 L 1.415,12.541 L 1.424,12.550 C 2.868,14.633 5.273,16.000 8.000,16.000 C 12.418,16.000 16.000,12.418 16.000,8.000 C 16.000,3.582 12.418,0.000 8.000,0.000 Z'><Path.Fill><LinearGradientBrush MappingMode='Absolute' StartPoint='0,8' EndPoint='16,8'><GradientStop Offset='0' Color='red'/><GradientStop Offset='1' Color='#FF850000'/></LinearGradientBrush></Path.Fill></Path></Canvas></DataTemplate>";

#if SILVERLIGHT
      m_noDropCursorTemplate = XamlReader.Load( cursorXaml ) as DataTemplate;
#else
      m_noDropCursorTemplate = XamlReader.Parse( cursorXaml ) as DataTemplate;
#endif
    }

    #region CurrentDragContext

    public static DragDropContext CurrentDragDropContext
    {
      get
      {
        DragDropManager manager = DragDropManager.SingletonInstance;
        if( manager != null && manager.m_isDragActive )
          return manager.m_dragContext;

        return null;
      }
    }

    private DragDropContext m_dragContext;

    #endregion

    #region Instance Static Property

    internal static DragDropManager Instance
    {
      get
      {
        if( DragDropManager.SingletonInstance == null )
        {
          DragDropManager.SingletonInstance = new DragDropManager();
        }

        return DragDropManager.SingletonInstance;
      }
    }

    private static DragDropManager SingletonInstance;

    #endregion

    #region DraggedOverReference Property

    internal static IDropTarget DraggedOverReference
    {
      get
      {
        if( ( DragDropManager.Instance.m_draggedOverReference != null ) && ( DragDropManager.Instance.m_draggedOverReference.IsAlive ) )
        {
          return DragDropManager.Instance.m_draggedOverReference.Target as IDropTarget;
        }

        return null;
      }
    }
    private WeakReference m_draggedOverReference; // null

    #endregion

    #region IsDragActive Property

    internal static bool IsDragActive
    {
      get
      {
        return DragDropManager.Instance.m_isDragActive;
      }
    }
    private bool m_isDragActive; // false

    #endregion

    public static void PrepareDrag( DragDropContext dragContext, MouseButtonEventArgs e )
    {
      DragDropManager.Instance.PrepareDragWorker( dragContext, e );
    }

    public static void AddDropTarget( IDropTarget dropTarget )
    {
      DragDropManager.Instance.AddDropTargetWorker( dropTarget );
    }

    public static void RemoveDropTarget( IDropTarget dropTarget )
    {
      DragDropManager.Instance.RemoveDropTargetWorker( dropTarget );
    }

    public static void CancelDrag()
    {
      DragDropManager.Instance.CancelDragCore();
    }

    public static void PauseDrag()
    {
      DragDropManager.Instance.PauseDragCore();
    }

    public static void ResumeDrag()
    {
      DragDropManager.Instance.ResumeDragCore();
    }

    private void PrepareDragWorker( DragDropContext dragContext, MouseButtonEventArgs e )
    {
      if( dragContext == null )
        throw new ArgumentNullException( "dragContext" );

      m_dragContext = dragContext;

      if( m_isDragActive )
        throw new InvalidOperationException( "An attempt was made to drag an element while one is already being dragged." );

      UIElement draggedElement = m_dragContext.DraggedElement;

      if( draggedElement == null )
        throw new ArgumentException( "The dragContext's DraggedElement cannot be null." );

      if( e == null )
        throw new ArgumentNullException( "e" );


      draggedElement.MouseMove += this.OnDraggedElementMouseMove;
      draggedElement.MouseLeftButtonUp += this.OnDraggedElementMouseLeftButtonUp;
      draggedElement.MouseEnter += this.OnDraggedElementMouseEnter;
      draggedElement.MouseLeave += this.OnDraggedElementMouseLeave;
      draggedElement.LostMouseCapture += this.OnDraggedElementLostMouseCapture;

      m_mouseLeftButtonDownLocation = e.GetPosition( null );
      dragContext.MouseOffset = e.GetPosition( draggedElement );
      m_lastMousePosition = m_mouseLeftButtonDownLocation;
    }

    private void AddDropTargetWorker( IDropTarget dropTarget )
    {
      WeakReference dropTargetWeakRef = this.FindDropTarget( dropTarget );

      if( dropTargetWeakRef != null )
        return;

      FrameworkElement uiElement = dropTarget as FrameworkElement;

      if( uiElement == null )
        throw new ArgumentException( "The specified IDropTarget must derive from FrameworkElement.", "dropTarget" );

      dropTargetWeakRef = new WeakReference( dropTarget );
      m_dropTargets.Add( dropTargetWeakRef );
    }

    private void RemoveDropTargetWorker( IDropTarget dropTarget )
    {
      WeakReference dropTargetWeakRef = this.FindDropTarget( dropTarget );

      if( dropTargetWeakRef == null )
        return;

      m_dropTargets.Remove( dropTargetWeakRef );
    }

    private WeakReference FindDropTarget( IDropTarget dropTarget )
    {
      WeakReference retval = null;

      foreach( WeakReference dropTargetReference in m_dropTargets )
      {
        if( dropTargetReference.IsAlive )
        {
          if( dropTargetReference.Target == dropTarget )
          {
            retval = dropTargetReference;
            break;
          }
        }
        else
        {
          this.QueueReferenceForCleanup( dropTargetReference );
        }
      }

      return retval;
    }

    private void QueueReferenceForCleanup( WeakReference dropTargetReference )
    {
      if( !m_deadReferences.Contains( dropTargetReference ) )
      {
        m_deadReferences.Add( dropTargetReference );
      }

      if( m_cleanupDispatcherOperation == null )
      {
        Dispatcher dispatcher;

#if SILVERLIGHT
        dispatcher = Application.Current.RootVisual.Dispatcher;
#else
        dispatcher = Dispatcher.CurrentDispatcher;
#endif
        m_cleanupDispatcherOperation = dispatcher.BeginInvoke( new Action( CleanDeadReferences ) );
      }
    }

    private void CleanDeadReferences()
    {
      foreach( WeakReference deadWeakRef in m_deadReferences )
      {
        m_dropTargets.Remove( deadWeakRef );
      }

      m_deadReferences.Clear();

      m_cleanupDispatcherOperation = null;
    }

    private void InvokeDragEnter( IDropTarget dropTarget )
    {
      if( dropTarget == null )
        return;

      dropTarget.DragEnter( m_dragContext );
    }

    private void InvokeDragLeave( IDropTarget dropTarget )
    {
      if( dropTarget == null )
        return;

      dropTarget.DragLeave( m_dragContext );
    }

    private void InvokeDragOver( IDropTarget dropTarget, Point mousePosition )
    {
      if( dropTarget == null )
        return;

      dropTarget.DragOver( m_dragContext, mousePosition );
    }

    private void InvokeDragDrop( IDropTarget dropTarget )
    {
      if( dropTarget == null )
        return;

      dropTarget.Drop( m_dragContext );
    }

    private void BeginDrag()
    {
      if( m_isDragActive )
        return;

      DragDropCancelEventArgs eventArgs = new DragDropCancelEventArgs( m_dragContext );

      m_dragContext.OnDragStarting( eventArgs );

      if( eventArgs.Cancel )
        return;

      //Freeze the DragDropContext to ensure that any "data" that must not be changed after the beginning of the operation, stays the same.
      m_dragContext.FreezeDragContext();

      //Try to capture the mouse, if it fails, throw... It can only fail if:
      //  * The mouse is NOT over the Silverlight plug-in content area.
      //  * Another Silverlight object has already captured the mouse.
      //  * Another non-Silverlight object has already captured the mouse.
      //  * The left mouse button is NOT the pressed state.
      FrameworkElement draggedElement = m_dragContext.DraggedElement;

      if( !draggedElement.CaptureMouse() )
        return; //no drag to be started if capture is not possible.

      DragDropAdornerManager.PrepareAdorner( m_dragContext, m_dragContext.LastMouseMoveEventArgs );

      m_isDragActive = true;

      m_previousCursor = draggedElement.Cursor;
      draggedElement.Cursor = Cursors.Hand;

      m_dragContext.OnDragStarted();
    }

    private void EndDrag()
    {
      if( m_isDragActive )
      {
        if( this.DropOnTarget() )
        {
          if( m_isDragActive )
          {
            m_dragContext.OnDragCompleted( new DragDropEventArgs( m_dragContext ) );
          }
        }
        else
        {
          if( m_isDragActive )
          {
            m_dragContext.OnDragAborted( new DragDropEventArgs( m_dragContext ) );
          }
        }
      }

      this.CleanDrag();
    }

    private bool DropOnTarget()
    {
      bool canDrop = false;

      if( !m_isDragActive || m_draggedOverReference == null )
        return canDrop;

      //Drop the element on the current target if it is possible
      IDropTarget dropTarget = m_draggedOverReference.Target as IDropTarget;
      if( dropTarget == null )
        return canDrop;

      canDrop = dropTarget.CanDropElement( m_dragContext );

      if( canDrop )
      {
        this.InvokeDragDrop( dropTarget );
      }

      return canDrop;
    }

    private void CleanDrag()
    {
      if( m_dragContext != null )
      {
        FrameworkElement draggedElement = m_dragContext.DraggedElement;
        draggedElement.MouseMove -= OnDraggedElementMouseMove;
        draggedElement.MouseLeftButtonUp -= OnDraggedElementMouseLeftButtonUp;
        draggedElement.MouseEnter -= OnDraggedElementMouseEnter;
        draggedElement.MouseLeave -= OnDraggedElementMouseLeave;
        draggedElement.LostMouseCapture -= OnDraggedElementLostMouseCapture;

        //Release the mouse capture only if a BeginDrag has set the mouse capture earlier.
        if( m_isDragActive )
        {
          draggedElement.ReleaseMouseCapture();
        }

        draggedElement.Cursor = m_previousCursor;
      }

      DragDropAdornerManager.HideAdorner();

      m_previousCursor = null;
      m_customCursorPopup.IsOpen = false;

      m_draggedOverReference = null;

      m_isDragActive = false;
      m_dragContext = null;
    }

    private void CancelDragCore()
    {
      if( m_isDragActive )
      {
        m_dragContext.OnDragAborted( new DragDropEventArgs( m_dragContext ) );
      }

      this.CleanDrag();
    }

    private void PauseDragCore()
    {
      if( m_isDragActive )
      {
        m_dragContext.OnDragPaused( new DragDropEventArgs( m_dragContext ) );
      }
    }

    private void ResumeDragCore()
    {
      if( m_isDragActive )
      {
        m_dragContext.OnDragResumed( new DragDropEventArgs( m_dragContext ) );
      }
    }

    private void UpdateCursor( Point mouseLocation,
      DragDropCursor cursor,
      DataTemplate customCursorTemplate )
    {
      bool isCustomCursor = false;

      FrameworkElement draggedElement = m_dragContext.DraggedElement as FrameworkElement;

      if( draggedElement == null )
        return;

      //Display the given cursor
      switch( cursor )
      {
        case DragDropCursor.Arrow:
          draggedElement.Cursor = Cursors.Arrow;
          break;

        case DragDropCursor.Hand:
          draggedElement.Cursor = Cursors.Hand;
          break;

        case DragDropCursor.IBeam:
          draggedElement.Cursor = Cursors.IBeam;
          break;

        case DragDropCursor.None:
          draggedElement.Cursor = Cursors.None;
          break;

        case DragDropCursor.SizeNS:
          draggedElement.Cursor = Cursors.SizeNS;
          break;

        case DragDropCursor.SizeWE:
          draggedElement.Cursor = Cursors.SizeWE;
          break;

        case DragDropCursor.Wait:
          draggedElement.Cursor = Cursors.Wait;
          break;

#if SILVERLIGHT
        case DragDropCursor.Eraser:
          draggedElement.Cursor = Cursors.Eraser;
          break;

        case DragDropCursor.No:
          //Workaround to simulate the "No" cursor
          draggedElement.Cursor = Cursors.None;
          isCustomCursor = true;

          if( customCursorTemplate == null )
            customCursorTemplate = m_noDropCursorTemplate;

          break;

        case DragDropCursor.Stylus:
          draggedElement.Cursor = Cursors.Stylus;
          break;
#else
        case DragDropCursor.Help:
          draggedElement.Cursor = Cursors.Help;
          break;

        case DragDropCursor.No:
          draggedElement.Cursor = Cursors.No;
          break;
#endif
        case DragDropCursor.Custom:
          draggedElement.Cursor = Cursors.None;
          isCustomCursor = true;
          break;

        default:
          draggedElement.Cursor = Cursors.Arrow;
          break;
      }

      // Use a custom cursor (hack) to display the custom cursor
      // from the DataTemplate
      m_customCursorPopup.IsOpen = isCustomCursor;

      if( isCustomCursor )
      {
        ContentPresenter contentPresenter = m_customCursorPopup.Child as ContentPresenter;

        if( contentPresenter != null )
          contentPresenter.ContentTemplate = customCursorTemplate;

        m_customCursorPopup.VerticalOffset = mouseLocation.Y;
        m_customCursorPopup.HorizontalOffset = mouseLocation.X;
      }
    }

    private void OnDraggedElementMouseMove( object sender, MouseEventArgs e )
    {
      m_dragContext.LastMouseMoveEventArgs = e;

      //check if the minimum threshold is attain to start dragging the element
      if( !m_isDragActive )
      {
        Point mouseLocation = e.GetPosition( null );
        if( DragDropHelper.IsMouseMoveDrag( m_mouseLeftButtonDownLocation, mouseLocation ) )
        {
          this.BeginDrag();
        }
      }

      if( m_isDragActive )
      {
        this.PerformDragOperationHitTest( e );
        DragDropAdornerManager.OnAdornedElementMouseMove( e );
      }
    }

    private void OnDraggedElementMouseLeftButtonUp( object sender, MouseButtonEventArgs e )
    {
      if( m_isDragActive )
      {
        this.EndDrag();
      }
      else
      {
        this.CleanDrag();
      }
    }

    private void OnDraggedElementMouseEnter( object sender, MouseEventArgs e )
    {
      if( !m_isDragActive )
      {
        this.CleanDrag();
      }
    }

    private void OnDraggedElementMouseLeave( object sender, MouseEventArgs e )
    {
      if( !m_isDragActive )
      {
        this.CleanDrag();
      }
    }

    private void OnDraggedElementLostMouseCapture( object sender, MouseEventArgs e )
    {
      if( !m_isDragActive )
        return;

      //From here, we know that a ReleaseMouseCapture has been called on the dragged element
      //outside of the Drag & Drop manager.  In that case, we must abort the whole operation.
      m_dragContext.OnDragAborted( new DragDropEventArgs( m_dragContext ) );
      this.CleanDrag();
    }

    private void PerformDragOperationHitTest( MouseEventArgs e )
    {
      if( !m_isDragActive )
        return;

      Func<UIElement, Point> getRelativeLocation = element => e.GetPosition( element );
      bool canDrop = false;

      //Cleanup previous dragged over reference if there was no hit
      if( !this.HitTestDropTargets( getRelativeLocation, out canDrop ) )
      {
        if( m_draggedOverReference != null )
        {
          //Issue a DragLeave for the previously dragged over element
          this.InvokeDragLeave( m_draggedOverReference.Target as IDropTarget );

          m_draggedOverReference = null;
        }
      }

      //Notify the "owner" of the drag operation that the mouse has moved
      Point mouseLocation = e.GetPosition( null );

      // We need to reverse the X axis in case the dragged element's flow direction
      // is RightToLeft. We do not support having different flow direction inside a grid.

#if !WINDOWS_PHONE
      Point mouseRelativeLocation = ( m_dragContext.DraggedElement.FlowDirection == FlowDirection.LeftToRight )
        ? new Point( mouseLocation.X - m_lastMousePosition.X, mouseLocation.Y - m_lastMousePosition.Y )
        : new Point( m_lastMousePosition.X - mouseLocation.X, mouseLocation.Y - m_lastMousePosition.Y );
#else
      Point mouseRelativeLocation = new Point( mouseLocation.X - m_lastMousePosition.X, mouseLocation.Y - m_lastMousePosition.Y );
#endif

      DragDropCursor cursor = m_dragContext.GetCursor( canDrop );

      DataTemplate customCursorTemplate = null;

      DragDropMoveEventArgs dragDropEventArgs = new DragDropMoveEventArgs( m_dragContext, mouseRelativeLocation, cursor );

      m_dragContext.OnDragMove( dragDropEventArgs );

      if( dragDropEventArgs.Cursor != cursor )
      {
        cursor = dragDropEventArgs.Cursor;
      }

      if( cursor == DragDropCursor.Custom )
        customCursorTemplate = m_dragContext.GetCustomCursorTemplate( canDrop );

      this.UpdateCursor( mouseLocation, cursor, customCursorTemplate );

      m_lastMousePosition = mouseLocation;
    }

    private bool HitTestDropTargets( Func<UIElement, Point> getRelativeLocation, out bool canDrop )
    {
      canDrop = false;
      bool hasHit = false;

      if( getRelativeLocation == null )
        return hasHit;

      List<WeakReference> draggedOverElements = new List<WeakReference>();

      //Cycle through the valid elements for drag operation and try to hit test them with mouse position relative to application
      foreach( WeakReference dropTargetReference in m_dropTargets )
      {
        FrameworkElement dropTargetFE = dropTargetReference.Target as FrameworkElement;

        //Remove the drop target if it isn't available anymore
        if( dropTargetFE == null )
        {
          this.QueueReferenceForCleanup( dropTargetReference );
          continue;
        }

        Point dropTargetRelativePosition = getRelativeLocation.Invoke( dropTargetFE );

        if( !this.HitTestDropTarget( dropTargetRelativePosition, dropTargetFE ) )
        {
          continue;
        }

        draggedOverElements.Add( dropTargetReference );
      }

      // Get the TopMost IDropTarget from the one that were hit tested
      WeakReference topMostDraggedOverDropTargetReference = null;
      int draggedOverElementsCount = draggedOverElements.Count;

      if( draggedOverElementsCount == 0 )
      {
        return false;
      }
      else if( draggedOverElementsCount == 1 )
      {
        topMostDraggedOverDropTargetReference = draggedOverElements[ 0 ];
      }
      else if( draggedOverElementsCount > 1 )
      {
        topMostDraggedOverDropTargetReference = draggedOverElements[ 0 ];
        WeakReference secondElementReference = null;

        for( int i = 1; i < draggedOverElementsCount; i++ )
        {
          secondElementReference = draggedOverElements[ i ];

          FrameworkElement firstElement = topMostDraggedOverDropTargetReference.Target as FrameworkElement;
          FrameworkElement secondElement = secondElementReference.Target as FrameworkElement;

          if( firstElement.IsAncestor( secondElement ) )
          {
            topMostDraggedOverDropTargetReference = secondElementReference;
            continue;
          }
        }
      }

      // Process TopMost IDropTarget
      IDropTarget dropTarget = topMostDraggedOverDropTargetReference.Target as IDropTarget;
      bool allowDrop = dropTarget.CanDropElement( m_dragContext );
      canDrop |= allowDrop;

      //If the hit test returns an element other than the previously "dragged over" element
      if( topMostDraggedOverDropTargetReference != m_draggedOverReference )
      {
        if( m_draggedOverReference != null )
        {
          //Issue a DragLeave for the previously dragged over element
          this.InvokeDragLeave( m_draggedOverReference.Target as IDropTarget );
        }

        m_draggedOverReference = topMostDraggedOverDropTargetReference;

        if( allowDrop )
        {
          //Issue a DragEnter for the current target
          this.InvokeDragEnter( dropTarget );
        }
      }

      if( allowDrop )
      {
        //Issue the drag over message to drop target
        this.InvokeDragOver( dropTarget, getRelativeLocation.Invoke( dropTarget as UIElement ) );
      }

      return true;
    }

    private bool HitTestDropTarget( Point dropTargetRelativePosition, FrameworkElement dropTargetFE )
    {
      if( !dropTargetFE.IsHitTestVisible )
        return false;

      if( ( dropTargetRelativePosition.X >= 0d ) && ( dropTargetRelativePosition.X <= dropTargetFE.ActualWidth )
         && ( dropTargetRelativePosition.Y >= 0d ) && ( dropTargetRelativePosition.Y <= dropTargetFE.ActualHeight ) )
      {
        return true;
      }

      return false;
    }

    private List<WeakReference> m_dropTargets = new List<WeakReference>();
    private List<WeakReference> m_deadReferences = new List<WeakReference>();

    private DispatcherOperation m_cleanupDispatcherOperation; // null

    private Point m_mouseLeftButtonDownLocation;
    private Point m_lastMousePosition;

    private Popup m_customCursorPopup = new Popup();
    private DataTemplate m_noDropCursorTemplate; // = null;
    private Cursor m_previousCursor; // null
  }
}
